﻿using System;
using System.Collections.Generic;
using DatabaseGroup.SSIS.SourceAdapters.View;
using DatabaseGroup.SSIS.SourceAdapters.Model;
using Microsoft.SqlServer.Dts.Runtime.Wrapper;

namespace DatabaseGroup.SSIS.SourceAdapters.Presenter
{
    /// <summary>
    /// Presenter for handling the Advance tab view
    /// </summary>
    public class AdvanceTabPresenter
    {
        private readonly IEditorAdvanceView _view;
        private readonly IComponentModel _model;
        private AdvanceTabSettings _settings;

        /// <summary>
        /// Initializes a new instance of the AdvanceTabPresenter class
        /// using the view and outputModel
        /// </summary>
        /// <param name="view">An instance of an IEditorAdvanceView</param>
        /// <param name="model">An instance of an IComponentModel</param>
        public AdvanceTabPresenter(IEditorAdvanceView view, IComponentModel model)
        {
            _view = view;
            _model = model;

            InitializeView();

            view.ViewSettings += view_ViewSettings;
            view.SavingSettings += view_SavingSettings;
            view.SelectedAdvanceColumnChanged += view_SelectedAdvanceColumnChanged;
            view.AddNewAdvanceColumn += view_AddNewAdvanceColumn;
            view.AddNewAdvanceColumnAfter += view_AddNewAdvanceColumnAfter;
            view.AddNewAdvanceColumnBefore += view_AddNewAdvanceColumnBefore;
            view.RemoveAdvanceColumn += view_RemoveAdvanceColumn;
        }

        /// <summary>
        /// Removes the currently selected column from the list
        /// of configured columns
        /// </summary>
        /// <param name="sender">The View</param>
        /// <param name="e"></param>
        private void view_RemoveAdvanceColumn(object sender, IndexEventArgs e)
        {
            if (e.Index < 0)
                return;

            _settings.Columns.RemoveAt(e.Index);
            _view.BindAdvanceTab(_settings);

            if (e.Index > 0)
            {
                _view.ViewAdvanceColumnProperties(e.Index - 1, _settings.Columns[e.Index - 1]);
            }
            else if (_settings.Columns.Count > 0)
            {
                _view.ViewAdvanceColumnProperties(0, _settings.Columns[0]);
            }
            else
            {
                _view.ViewAdvanceColumnProperties(-1, null);
            }
        }

        /// <summary>
        /// Adds a new column configuration immediately before the
        /// currently selected column
        /// </summary>
        /// <param name="sender">The View</param>
        /// <param name="e"></param>
        private void view_AddNewAdvanceColumnBefore(object sender, IndexEventArgs e)
        {
            int insertAt = _settings.Columns.Count;

            if (e.Index >= 0)
                insertAt = e.Index;

            AddNewColumn(insertAt);
        }

        /// <summary>
        /// Adds a new column configuration immediately after the
        /// currently selected column
        /// </summary>
        /// <param name="sender">The View</param>
        /// <param name="e"></param>
        private void view_AddNewAdvanceColumnAfter(object sender, IndexEventArgs e)
        {
            int insertAt = _settings.Columns.Count;

            if (e.Index >= 0)
                insertAt = e.Index + 1;

            AddNewColumn(insertAt);
        }

        /// <summary>
        /// Adds a new column configuration to the end of the list of
        /// configured columns
        /// </summary>
        /// <param name="sender">The View</param>
        /// <param name="e"></param>
        private void view_AddNewAdvanceColumn(object sender, EventArgs e)
        {
            AddNewColumn(_settings.Columns.Count);
        }

        /// <summary>
        /// Update the advance column properties being viewed to match
        /// the newly selected column
        /// </summary>
        /// <param name="sender">The View</param>
        /// <param name="e"></param>
        private void view_SelectedAdvanceColumnChanged(object sender, IndexEventArgs e)
        {
            if (e.Index >= 0 && e.Index < _settings.Columns.Count)
                _view.ViewAdvanceColumnProperties(e.Index, _settings.Columns[e.Index]);
        }

        /// <summary>
        /// Binds the view with the current settings object
        /// </summary>
        /// <param name="sender">The View</param>
        /// <param name="e"></param>
        private void view_ViewSettings(object sender, TabEventArgs e)
        {
            if (e.Tab != EditorViewTab.Advance && e.Tab != EditorViewTab.ANY)
                return;

            HookupColumnEvents();
            _view.BindAdvanceTab(_settings);

            if (_settings.Columns.Count > 0)
                _view.ViewAdvanceColumnProperties(0, _settings.Columns[0]);
            else
                _view.ViewAdvanceColumnProperties(-1, null);
        }

        /// <summary>
        /// Saves the current settings to the component metadata
        /// </summary>
        /// <param name="sender">The View</param>
        /// <param name="e"></param>
        private void view_SavingSettings(object sender, EventArgs e)
        {
            CommitColumnPropertiesToComponent(_model, _settings.Columns);
        }

        /// <summary>
        /// Loops through the list of column objects ensuring that
        /// the presenter is listening to the Updating event
        /// </summary>
        private void HookupColumnEvents()
        {
            foreach (ColumnProperty column in _settings.Columns)
            {
                try
                {
                    // attempt to removing any previous hooks which may have already been setup
                    column.Updating -= column_Updating;
                }
                finally
                {
                    column.Updating += column_Updating;
                }
            }
        }

        /// <summary>
        /// Validates that the value being set on a column property
        /// is allowed based on the column properties
        /// </summary>
        /// <param name="sender">The column</param>
        /// <param name="e"></param>
        private void column_Updating(object sender, CancelPropertyUpdateEventArgs e)
        {
            ColumnProperty column = (ColumnProperty)sender;

            // TODO: break switch into separate validation methods
            switch (e.PropertyName)
            {
                case "DataType":
                    SetDefaultColumnDataTypeProperties(column, column.DataType);
                    break;
                case "Name":
                    if (string.IsNullOrEmpty((string)e.NewValue) || _settings.Columns.FindAll(col => col.Name == (string) e.NewValue).Count > 1)
                        e.Cancel = true;
                    break;
                case "DataPrecision":
                    if (column.DataType != DataType.DT_NUMERIC && column.DataType != DataType.DT_DECIMAL && (int)e.NewValue != 0)
                        e.Cancel = true;
                    else if ((column.DataType == DataType.DT_NUMERIC || column.DataType == DataType.DT_DECIMAL) &&
                        column.DataScale >= (int)e.NewValue)
                        e.Cancel = true;
                    break;
                case "DataScale":
                    if (column.DataType != DataType.DT_NUMERIC && column.DataType != DataType.DT_DECIMAL && (int)e.NewValue != 0)
                        e.Cancel = true;
                    else if ((column.DataType == DataType.DT_NUMERIC || column.DataType == DataType.DT_DECIMAL) &&
                        (int)e.NewValue >= column.DataPrecision)
                        e.Cancel = true;
                    break;
                case "OutputColumnWidth":
                    if (column.DataType == DataType.DT_STR && ((int)e.NewValue < 1 || (int)e.NewValue > 8000))
                        e.Cancel = true;
                    else if (column.DataType == DataType.DT_WSTR && ((int)e.NewValue < 1 || (int)e.NewValue > 4000))
                        e.Cancel = true;
                    else if (column.DataType != DataType.DT_WSTR && column.DataType != DataType.DT_STR && (int)e.NewValue != 0)
                        e.Cancel = true;
                    break;
                case "CodePage":
                    if (column.DataType == DataType.DT_STR && (int)e.NewValue < 1)
                        e.Cancel = true;
                    else if (column.DataType != DataType.DT_STR && (int)e.NewValue != 0)
                        e.Cancel = true;
                    break;
                // TODO: sort out the TypeConverter so this isn't required!
                case "ErrorHandling":
                case "TruncationHandling":
                    if ((string)e.NewValue != "Ignore and log failure" && (string)e.NewValue != "Redirect row" && (string)e.NewValue != "Fail component")
                        e.Cancel = true;
                    break;
            }

            if (e.Cancel)
                _view.ShowAlert("Property setting is invalid");
            else
            {
                int topIndex = _view.AdvanceColumnTopIndex;
                _view.BindAdvanceTab(_settings);
                _view.AdvanceColumnTopIndex = topIndex;
            }
        }

        /// <summary>
        /// Creates a new advance settings object and loads it with the current
        /// component configuration and binds it to the view
        /// </summary>
        private void InitializeView()
        {
            _settings = new AdvanceTabSettings();
            _settings.Columns = BuildColumnPropertiesFromComponent(_model);
            _view.BindAdvanceTab(_settings);
        }

        /// <summary>
        /// Sets the default core column properties based on the datatype
        /// </summary>
        /// <param name="column">The column to set the properties on</param>
        /// <param name="datatype">The datatype to use</param>
        private void SetDefaultColumnDataTypeProperties(ColumnProperty column, DataType datatype)
        {
            int length = 0;
            int precision = 0;
            int codePage = 0;

            switch (datatype)
            {
                case DataType.DT_STR:
                    codePage = column.CodePage > 0 ? column.CodePage : 1252;
                    length = column.OutputColumnWidth > 0 ? column.OutputColumnWidth : 255;
                    break;
                case DataType.DT_WSTR:
                    length = column.OutputColumnWidth > 0 ? column.OutputColumnWidth : 255;
                    break;
                case DataType.DT_NUMERIC:
                    precision = 18;
                    break;
            }

            column.SetCoreColumnProperties(datatype, length, 0, precision, codePage);
        }

        /// <summary>
        /// Adds a new column to the list of configured columns 
        /// at the specific column index
        /// </summary>
        /// <param name="insertAt">The zero-based index to insert the column at</param>
        private void AddNewColumn(int insertAt)
        {
            GeneralTabSettings generalSettings = _view.GetTabSettings<GeneralTabSettings>(EditorViewTab.General);
            bool useUnicode = PresenterUtils.ConvertStringToCodepage(generalSettings.CodePage) == 65001;

            if (generalSettings.IsUnicode)
                useUnicode = true;

            ColumnProperty column = PresenterUtils.CreateDefaultColumnProperty(useUnicode);

            // TODO: MessageStrings
            string name = "Column ";
            int index = 1;

            while (_settings.Columns.Exists((col) => col.Name == name + index))
            {
                index++;
            }

            column.Name = name + index;
            column.Updating += column_Updating;
            _settings.Columns.Insert(insertAt, column);
            _view.BindAdvanceTab(_settings);
            _view.ViewAdvanceColumnProperties(insertAt, column);
        }

        /// <summary>
        /// Builds a list of column properties based on the saved list
        /// of configured file columns held within the component
        /// </summary>
        /// <param name="componentModel">The component model object</param>
        /// <returns>The configured list of column properties</returns>
        private List<ColumnProperty> BuildColumnPropertiesFromComponent(IComponentModel componentModel)
        {
            string fileColumnsXML = componentModel.GetProperty<string>(PropertiesManager.ConfiguredFileColumnsPropName);
            List<ConfiguredFileColumn> fileColumns = CommonUtils.DeserializeConfiguredFileColumns(fileColumnsXML);

            List<ColumnProperty> columns = new List<ColumnProperty>();

            foreach (ConfiguredFileColumn column in fileColumns)
            {
                ColumnProperty viewColumn =
                    new ColumnProperty(column.Name, column.Precision, column.Scale, column.DataType, column.Length, column.CodePage);
                viewColumn.IsForOutput = column.IsOutput;
                viewColumn.ErrorHandling = "Redirect row";
                viewColumn.TruncationHandling = "Ignore and log failure";

                columns.Add(viewColumn);
            }

            return columns;
        }

        /// <summary>
        /// Takes a list of column properties and saves them to 
        /// the component as a list of configured file columns
        /// </summary>
        /// <param name="model">The component model to commit the columns to</param>
        /// <param name="columns">The columns to commit</param>
        private void CommitColumnPropertiesToComponent(IComponentModel model, List<ColumnProperty> columns)
        {
            model.ClearStandardOutputColumns();

            List<ConfiguredFileColumn> fileColumns = new List<ConfiguredFileColumn>();

            foreach (ColumnProperty column in columns)
            {
                ConfiguredFileColumn fileColumn = new ConfiguredFileColumn()
                {
                    CodePage = column.CodePage,
                    DataType = column.DataType,
                    IsOutput = column.IsForOutput,
                    Length = column.OutputColumnWidth,
                    Name = column.Name,
                    Precision = column.DataPrecision,
                    Scale = column.DataScale
                };

                fileColumns.Add(fileColumn);

                if (column.IsForOutput)
                    model.InsertStandardOutputColumn(fileColumn);
            }

            model.SetProperty(PropertiesManager.ConfiguredFileColumnsPropName, CommonUtils.SerializeConfiguredFileColumns(fileColumns));
        }
    }
}
